user13906415
user13906415

Reputation:

webgl performance differences between firefox and chrome

I am currently developing an image processing tool for a webapplication. I need to take to png-images of the same size and combine them pixel by pixel. So far, I have set up a prototype (very much inspired by the tutorials on webglfundamentals.org) that takes two images and just multiplies their pixels. I am using the twgl-helper library for webgl from http://twgljs.org/ . (Which I could unfortunately not put into the fiddle).

I have the following question: Can anyone explain or give hints, why Firefox 78 is so much slower in this than a recent Chrome? FF averages about 34ms per render (complete refresh and wipe cash between samples) while Chrome averages 0.27ms per render. This is two orders of magnitude of a difference which i just can't explain. I have tried webgl2, it is slightly faster for both but keeps the insane difference between the two.

If I need to provide more info, pls let me know, I will be back in the office on thursday. Thank you for your support and ideas.

function main() {


    // Get A WebGL context
    var canvas = document.getElementById("webgl");
    var gl = canvas.getContext("webgl");
    if (!gl) {
        return;
    }

    var canvas1 = document.getElementById("canvas1");
    var canvas2 = document.getElementById("canvas2");

    // setup GLSL program
    var program = twgl.createProgramFromScripts(gl, ["2d-vertex-shader", "2d-fragment-shader"]);
    gl.useProgram(program);

    var time0 = performance.now();
    // look up where the vertex data needs to go.
    var positionLocation = gl.getAttribLocation(program, "a_position");
    var texCoordLocation = gl.getAttribLocation(program, "a_texCoord");

    // provide texture coordinates for the rectangle.
    var texCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);

    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
        0.0, 0.0,
        1.0, 0.0,
        0.0, 1.0,
        0.0, 1.0,
        1.0, 0.0,
        1.0, 1.0]), gl.STATIC_DRAW);

    // vertex attributes need to be turned on explicitly
    gl.enableVertexAttribArray(texCoordLocation);
    gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);

    // lookup uniforms
    var resolutionLocation = gl.getUniformLocation(program, "u_resolution");

    // set the resolution
    gl.uniform2f(resolutionLocation, canvas1.width, canvas1.height);

    // Create a buffer for the position of the rectangle corners.
    var buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.enableVertexAttribArray(positionLocation);
    gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

    // Set a rectangle the same size as the image.
    setRectangle(gl, 0, 0, canvas.width, canvas.height);
    // setRectangle(gl, 0, 0, 1000, 1000);


    function setupTexture(canvas, textureUnit, program, uniformName) {
        var tex = gl.createTexture();

        updateTextureFromCanvas(tex, canvas, textureUnit);

        // Set the parameters so we can render any size image.
        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_MAG_FILTER, gl.LINEAR);
        // mal ausprobieren
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

        var location = gl.getUniformLocation(program, uniformName);
        gl.uniform1i(location, textureUnit);
    }

    function updateTextureFromCanvas(tex, canvas, textureUnit) {
        gl.activeTexture(gl.TEXTURE0 + textureUnit);
        gl.bindTexture(gl.TEXTURE_2D, tex);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
    }

    var tex1 = setupTexture(canvas1, 0, program, "u_canvas1");
    var tex2 = setupTexture(canvas2, 1, program, "u_canvas2");

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 6);

    var time1 = performance.now();
    console.log("Processing image took " + (time1 - time0) + " ms.");
    document.getElementById("performance").innerHTML = "Processing image took " + (time1 - time0) + " ms.";
}

function setRectangle(gl, x, y, width, height) {
    var x1 = x;
    var x2 = x + width;
    var y1 = y;
    var y2 = y + height;
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
        x1, y1,
        x2, y1,
        x1, y2,
        x1, y2,
        x2, y1,
        x2, y2]), gl.STATIC_DRAW);
}

const WIDTH = 1600;
const HEIGHT = 900;

addNewImage = function (path, id, width, height) {
    console.log(path)
    let newElement = document.createElement("canvas");
    document.body.appendChild(newElement);
    newElement.id = id;
    let ctx = newElement.getContext("2d");
    ctx.canvas.width = width;
    ctx.canvas.height = height;
    let input = new Image();
    input.crossOrigin = "anonymous";
    input.onload = function () {
        ctx.drawImage(input, 0, 0);
    }
    input.src = path;
}



addNewImage("https://i.imgur.com/KjUybBD.png", "canvas1", WIDTH, HEIGHT);
addNewImage("https://i.imgur.com/ZKMnXce.png", "canvas2", WIDTH, HEIGHT);
canvas {
    border: 2px solid black;
    display: inline-block;
    width: 100%;
}
<script src="twgl.js"></script>
<link rel="stylesheet" type="text/css" href="style.css" />
<button onclick="main()">click</button>

<!-- vertex shader -->
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
attribute vec2 a_texCoord;

uniform vec2 u_resolution;

varying vec2 v_texCoord;

void main() {
   // convert the rectangle from pixels to 0.0 to 1.0
   vec2 zeroToOne = a_position / u_resolution;

   // convert from 0->1 to 0->2
   vec2 zeroToTwo = zeroToOne * 2.0;

   // convert from 0->2 to -1->+1 (clipspace)
   vec2 clipSpace = zeroToTwo - 1.0;

   gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);

   // pass the texCoord to the fragment shader
   // The GPU will interpolate this value between points.
   v_texCoord = a_texCoord;
}
</script>
<!-- fragment shader -->
<script id="2d-fragment-shader" type="x-shader/x-fragment">
    precision mediump float;

    // our 2 canvases
    uniform sampler2D u_canvas1;
    uniform sampler2D u_canvas2;

    // the texCoords passed in from the vertex shader.
    // note: we're only using 1 set of texCoords which means
    //   we're assuming the canvases are the same size.
    varying vec2 v_texCoord;

    void main() {
         // Look up a pixel from first canvas
         vec4 color1 = texture2D(u_canvas1, v_texCoord);

         // Look up a pixel from second canvas
         vec4 color2 = texture2D(u_canvas2, v_texCoord);

         // return the 2 colors multiplied
         gl_FragColor = color1 * color2;
    }
</script>

<!-- <canvas id="canvas1"></canvas>
<canvas id="canvas2"></canvas> -->
<div id="performance"></div>
<canvas id="webgl" width="1600" height="900"></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>

Upvotes: 2

Views: 3913

Answers (1)

user128511
user128511

Reputation:

It is not really possible to count on browsers having similar performance. There are plenty of tests where one browser is 2x to 40x faster than another.

In this particular case I don't know why modern Firefox is slower than Chrome. Chrome is multi-process (I thought Firefox was too at this point but maybe not) so in Chrome the timing is only timing how long it takes to insert commands into a command buffer to talk from the process that's running webpage to the separate process that talks to the GPU. It's not timing how long it actually takes to run those commands which run in parallel to the webpage.

If I add this after your draw call

// Force the webpage to wait for the GPU process
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(4));

Then I get comparable times for Chrome (27ms) vs Firefox (32ms)

function main() {


    // Get A WebGL context
    var canvas = document.getElementById("webgl");
    var gl = canvas.getContext("webgl");
    if (!gl) {
        return;
    }

    var canvas1 = document.getElementById("canvas1");
    var canvas2 = document.getElementById("canvas2");

    // setup GLSL program
    var program = twgl.createProgramFromScripts(gl, ["2d-vertex-shader", "2d-fragment-shader"]);
    gl.useProgram(program);

    var time0 = performance.now();
    // look up where the vertex data needs to go.
    var positionLocation = gl.getAttribLocation(program, "a_position");
    var texCoordLocation = gl.getAttribLocation(program, "a_texCoord");

    // provide texture coordinates for the rectangle.
    var texCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);

    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
        0.0, 0.0,
        1.0, 0.0,
        0.0, 1.0,
        0.0, 1.0,
        1.0, 0.0,
        1.0, 1.0]), gl.STATIC_DRAW);

    // vertex attributes need to be turned on explicitly
    gl.enableVertexAttribArray(texCoordLocation);
    gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);

    // lookup uniforms
    var resolutionLocation = gl.getUniformLocation(program, "u_resolution");

    // set the resolution
    gl.uniform2f(resolutionLocation, canvas1.width, canvas1.height);

    // Create a buffer for the position of the rectangle corners.
    var buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.enableVertexAttribArray(positionLocation);
    gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

    // Set a rectangle the same size as the image.
    setRectangle(gl, 0, 0, canvas.width, canvas.height);
    // setRectangle(gl, 0, 0, 1000, 1000);


    function setupTexture(canvas, textureUnit, program, uniformName) {
        var tex = gl.createTexture();

        updateTextureFromCanvas(tex, canvas, textureUnit);

        // Set the parameters so we can render any size image.
        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_MAG_FILTER, gl.LINEAR);
        // mal ausprobieren
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

        var location = gl.getUniformLocation(program, uniformName);
        gl.uniform1i(location, textureUnit);
    }

    function updateTextureFromCanvas(tex, canvas, textureUnit) {
        gl.activeTexture(gl.TEXTURE0 + textureUnit);
        gl.bindTexture(gl.TEXTURE_2D, tex);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
    }

    var tex1 = setupTexture(canvas1, 0, program, "u_canvas1");
    var tex2 = setupTexture(canvas2, 1, program, "u_canvas2");

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 6);
    // Force the webpage to wait for the GPU process
    gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(4));

    var time1 = performance.now();
    console.log("Processing image took " + (time1 - time0) + " ms.");
    document.getElementById("performance").innerHTML = "Processing image took " + (time1 - time0) + " ms.";
}

function setRectangle(gl, x, y, width, height) {
    var x1 = x;
    var x2 = x + width;
    var y1 = y;
    var y2 = y + height;
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
        x1, y1,
        x2, y1,
        x1, y2,
        x1, y2,
        x2, y1,
        x2, y2]), gl.STATIC_DRAW);
}

const WIDTH = 1600;
const HEIGHT = 900;

addNewImage = function (path, id, width, height) {
    console.log(path)
    let newElement = document.createElement("canvas");
    document.body.appendChild(newElement);
    newElement.id = id;
    let ctx = newElement.getContext("2d");
    ctx.canvas.width = width;
    ctx.canvas.height = height;
    let input = new Image();
    input.crossOrigin = "anonymous";
    input.onload = function () {
        ctx.drawImage(input, 0, 0);
    }
    input.src = path;
}



addNewImage("https://i.imgur.com/KjUybBD.png", "canvas1", WIDTH, HEIGHT);
addNewImage("https://i.imgur.com/ZKMnXce.png", "canvas2", WIDTH, HEIGHT);
canvas {
    border: 2px solid black;
    display: inline-block;
    width: 100%;
}
<script src="twgl.js"></script>
<link rel="stylesheet" type="text/css" href="style.css" />
<button onclick="main()">click</button>

<!-- vertex shader -->
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
attribute vec2 a_texCoord;

uniform vec2 u_resolution;

varying vec2 v_texCoord;

void main() {
   // convert the rectangle from pixels to 0.0 to 1.0
   vec2 zeroToOne = a_position / u_resolution;

   // convert from 0->1 to 0->2
   vec2 zeroToTwo = zeroToOne * 2.0;

   // convert from 0->2 to -1->+1 (clipspace)
   vec2 clipSpace = zeroToTwo - 1.0;

   gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);

   // pass the texCoord to the fragment shader
   // The GPU will interpolate this value between points.
   v_texCoord = a_texCoord;
}
</script>
<!-- fragment shader -->
<script id="2d-fragment-shader" type="x-shader/x-fragment">
    precision mediump float;

    // our 2 canvases
    uniform sampler2D u_canvas1;
    uniform sampler2D u_canvas2;

    // the texCoords passed in from the vertex shader.
    // note: we're only using 1 set of texCoords which means
    //   we're assuming the canvases are the same size.
    varying vec2 v_texCoord;

    void main() {
         // Look up a pixel from first canvas
         vec4 color1 = texture2D(u_canvas1, v_texCoord);

         // Look up a pixel from second canvas
         vec4 color2 = texture2D(u_canvas2, v_texCoord);

         // return the 2 colors multiplied
         gl_FragColor = color1 * color2;
    }
</script>

<!-- <canvas id="canvas1"></canvas>
<canvas id="canvas2"></canvas> -->
<div id="performance"></div>
<canvas id="webgl" width="1600" height="900"></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>

Of course the fact that Chrome is running the commands in another process means you get some parallel processing for free. I think most of the time this is a win for Chrome WebGL performance over Firefox WebGL performance but not always.

The only other thing that comes to mind for the remaining difference in speed is how the browsers transfer the canvas to texture. There are many possibilities

First off 2D canvases keep their data as premultiplied alpha but WebGL defaults to wanting un-premultiplied alpha so

  1. The browser has the 2D canvas in ram. It has to convert that data to unpremultiplied alpha then upload it via glTexImage2D. (Slow)

  2. The browser has the 2D canvas in vram. It downloads it to ram, converts it to unpremultiplied alpha then uploads it via glTexImage2D (Even Slower)

  3. The browser has the 2D canvas in vram. It attaches your texture to a framebuffer and renderers the canvas into it using a shader that unpremultiplies the alpha (fast).

I'm pretty positive Chrome does that last method. I know the code for it exists. I don't know all the conditions required to make sure that code is used but I'm pretty confident a 1900x600 canvas will take that path (at one point canvases below a certain size like 256x256 were done on the CPU, not the GPU but I have no idea if that is still true)

Firefox may or may not do the same thing but if it doesn't that could be why Chrome does this in 27ms and Firefox in 32ms when we stall the GPU process by calling gl.readPixels.

The larger point though is that browsers can optimize in many different ways and there is no guarantee which ways they will or won't optimize.

Upvotes: 4

Related Questions