<html>
<!--
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.

 Version 1.1
   - Added viewport rotation.
   - Adapted for talos with with 4 runs on combinations of alpha/antialias

 Version 1.0
   - Benoit Jacob's WebGL tutorial demos: http://bjacob.github.io/webgl-tutorial/12-texture.html
-->
<head>
<meta charset="utf-8"/>
<script src="resource://talos-powers/TalosContentProfiler.js"></script>
<script type="x-shader/x-vertex" id="vertexShader">
  attribute vec3 vertexPosition;
  attribute vec3 normalVector;
  attribute vec2 textureCoord;
  uniform mat4 modelview;
  uniform mat4 projection;
  varying vec3 varyingNormalVector;
  varying vec2 varyingTextureCoord;
  void main(void) {
    gl_Position = projection * modelview * vec4(vertexPosition, 1.0);
    varyingNormalVector = normalVector;
    varyingTextureCoord = textureCoord;
  }
</script>
<script type="x-shader/x-fragment" id="fragmentShader">
  precision mediump float;
  varying vec3 varyingNormalVector;
  uniform vec3 lightDirection;
  uniform sampler2D grassTextureSampler;
  varying vec2 varyingTextureCoord;
  void main(void) {
    vec3 grassColor = texture2D(grassTextureSampler, varyingTextureCoord).rgb;
    const float ambientLight = 0.3;
    const float diffuseLight = 0.7;
    float c = clamp(dot(normalize(varyingNormalVector), lightDirection), 0.0, 1.0);
    vec3 resultColor = grassColor * (c * diffuseLight + ambientLight);
    gl_FragColor = vec4(resultColor, 1);
  }
</script>
<script>
  var gl;

  var modelviewUniformLoc;
  var projectionUniformLoc;
  var lightDirectionUniformLoc;
  var grassTextureSamplerUniformLoc;

  var modelviewMatrix = new Float32Array(16);
  var projectionMatrix = new Float32Array(16);

  var terrainSize = 32;
  var aspectRatio;

  function startTest(alpha, antialias, doneCallback) {
    gl = document.getElementById("c").getContext("webgl", {alpha, antialias});

    var grassImage = document.getElementById("grass");
    var grassTexture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, grassTexture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, grassImage);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
    gl.generateMipmap(gl.TEXTURE_2D);

    var vertexShader = gl.createShader(gl.VERTEX_SHADER);
    var vertexShaderString = document.getElementById("vertexShader").text;
    gl.shaderSource(vertexShader, vertexShaderString);
    gl.compileShader(vertexShader);

    var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
    var fragmentShaderString = document.getElementById("fragmentShader").text;
    gl.shaderSource(fragmentShader, fragmentShaderString);
    gl.compileShader(fragmentShader);

    var program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);
    gl.useProgram(program);

    var vertexPositionAttrLoc = gl.getAttribLocation(program, "vertexPosition");
    gl.enableVertexAttribArray(vertexPositionAttrLoc);
    var normalVectorAttrLoc = gl.getAttribLocation(program, "normalVector");
    gl.enableVertexAttribArray(normalVectorAttrLoc);
    var textureCoordAttrLoc = gl.getAttribLocation(program, "textureCoord");
    gl.enableVertexAttribArray(textureCoordAttrLoc);

    modelviewUniformLoc = gl.getUniformLocation(program, "modelview");
    projectionUniformLoc = gl.getUniformLocation(program, "projection");
    lightDirectionUniformLoc = gl.getUniformLocation(program, "lightDirection");
    grassTextureSamplerUniformLoc = gl.getUniformLocation(program, "grassTextureSampler");

    var vertices = new Float32Array(terrainSize * terrainSize * 3);
    var normalVectors = new Float32Array(terrainSize * terrainSize * 3);
    var textureCoords = new Float32Array(terrainSize * terrainSize * 2);

    for (var i = 0; i < terrainSize; i++) {
      for (var j = 0; j < terrainSize; j++) {
        var a = 2 * Math.PI * i / terrainSize;
        var b = 2 * Math.PI * j / terrainSize;
        var height = 4 * Math.cos(a) + 6 * Math.sin(b) + Math.cos(4 * a) + Math.sin(5 * b);
        vertices[3 * (i + terrainSize * j) + 0] = i;
        vertices[3 * (i + terrainSize * j) + 1] = height;
        vertices[3 * (i + terrainSize * j) + 2] = j;

        var d_y_d_x = (2 * Math.PI / terrainSize) * (-4 * Math.sin(a) - 4 * Math.sin(4 * a));
        var d_y_d_z = (2 * Math.PI / terrainSize) * (6 * Math.cos(b) + 5 * Math.cos(5 * b));
        var normal_x = d_y_d_x;
        var normal_y = -1;
        var normal_z = d_y_d_z;
        var normal_length = Math.sqrt(normal_x * normal_x + normal_y * normal_y + normal_z * normal_z);
        normalVectors[3 * (i + terrainSize * j) + 0] = normal_x / normal_length;
        normalVectors[3 * (i + terrainSize * j) + 1] = normal_y / normal_length;
        normalVectors[3 * (i + terrainSize * j) + 2] = normal_z / normal_length;

        var textureRepeatingSpeed = 0.5;
        textureCoords[2 * (i + terrainSize * j) + 0] = i * textureRepeatingSpeed;
        textureCoords[2 * (i + terrainSize * j) + 1] = j * textureRepeatingSpeed;
      }
    }

    var vertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexPositionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
    gl.vertexAttribPointer(vertexPositionAttrLoc, 3, gl.FLOAT, false, 0, 0);

    var normalVectorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, normalVectorBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, normalVectors, gl.STATIC_DRAW);
    gl.vertexAttribPointer(normalVectorAttrLoc, 3, gl.FLOAT, false, 0, 0);

    var textureCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, textureCoords, gl.STATIC_DRAW);
    gl.vertexAttribPointer(textureCoordAttrLoc, 2, gl.FLOAT, false, 0, 0);

    var indexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    var indices = [];
    for (var k = 0; k < terrainSize - 1; k++) {
      for (var l = 0; l < terrainSize - 1; l++) {
        indices.push(k + terrainSize * l);
        indices.push(k + 1 + terrainSize * l);
        indices.push(k + 1 + terrainSize * (l + 1));

        indices.push(k + terrainSize * l);
        indices.push(k + 1 + terrainSize * (l + 1));
        indices.push(k + terrainSize * (l + 1));
      }
    }
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);

    gl.enable(gl.DEPTH_TEST);

    onresize();
    window.addEventListener("resize", onresize);

    startTestLoop(doneCallback);
  }

  function average(arr) {
    var sum = 0;
    for (var i in arr)
      sum += arr[i];
    return sum / (arr.length || 1);
  }

  // We use sample stddev and not population stddev because
  // well.. it's a sample and we can't collect all/infinite number of frames.
  function sampleStdDev(arr) {
    if (arr.length <= 1) {
      return 0;
    }
    var avg = average(arr);

    var squareDiffArr = arr.map( function(v) { return Math.pow(v - avg, 2); } );
    var sum = squareDiffArr.reduce( function(a, b) { return a + b; } );
    var rv = Math.sqrt(sum / (arr.length - 1));
    return rv;
  }

  const PRETEST_DELAY_MS  = 500;
  const WARMUP_TIMESTAMPS = 30; // Must be at least 2
  const MEASURED_FRAMES   = 100;

  var gDoneCallback = function placeholder(intervals) {};
  var gCurrentTimestamp = 0;
  var gResultTimestamps = [];

  function startTestLoop(doneCallback) {
    gDoneCallback = doneCallback;
    gCurrentTimestamp = 0;
    gResultTimestamps = new Array(WARMUP_TIMESTAMPS + MEASURED_FRAMES);

    TalosContentProfiler.resume("Starting requestAnimationFrame loop", true).then(() => {
      requestAnimationFrame(draw);
    });
  }

  function draw(timestamp) {
    // It's possible that under some implementations (even if not our current one),
    // the rAF callback arg will be in some way "optimized", e.g. always point to the
    // estimated next vsync timestamp, in order to allow the callee to have less
    // jitter in its time-dependent positioning/processing.
    // Such behaviour would harm our measurements, especially with vsync off.
    // performance.now() will not have this potential issue and is high-resolution.
    gResultTimestamps[gCurrentTimestamp++] = performance.now();
    var recordedTimestamps = gCurrentTimestamp;
    if (recordedTimestamps >= WARMUP_TIMESTAMPS + MEASURED_FRAMES) {
      TalosContentProfiler.pause("Stopping requestAnimationFrame loop", true).then(() => {
        var intervals = [];
        var lastWarmupTimestampId = WARMUP_TIMESTAMPS - 1;
        for (var i = lastWarmupTimestampId + 1; i < gResultTimestamps.length; i++) {
          intervals.push(gResultTimestamps[i] - gResultTimestamps[i - 1]);
        }

        gDoneCallback(intervals);
      });

      return;
    }

    // Used for rendering reproducible frames which are independent of actual performance (timestamps).
    var simulatedTimestamp = gCurrentTimestamp * 1000 / 60;

    var speed = 0.001;
    var angle =  simulatedTimestamp * speed;
    var c = Math.cos(angle / 10);
    var s = Math.sin(angle / 10);
    var light_x = Math.cos(angle);
    var light_y = -1;
    var light_z = Math.sin(angle);
    var l = Math.sqrt(light_x * light_x + light_y * light_y + light_z * light_z);
    light_x /= l;
    light_y /= l;
    light_z /= l;
    gl.uniform3f(lightDirectionUniformLoc, light_x, light_y, light_z);

    modelviewMatrix[0]  = c;
    modelviewMatrix[1]  = 0;
    modelviewMatrix[2]  = s;
    modelviewMatrix[3]  = 0;

    modelviewMatrix[4]  = 0;
    modelviewMatrix[5]  = 1;
    modelviewMatrix[6]  = 0;
    modelviewMatrix[7]  = 0;

    modelviewMatrix[8]  = -s;
    modelviewMatrix[9]  = 0;
    modelviewMatrix[10] = c;
    modelviewMatrix[11] = 0;

    modelviewMatrix[12] = -terrainSize / 2;
    modelviewMatrix[13] = 0;
    modelviewMatrix[14] = -terrainSize;
    modelviewMatrix[15] = 1;

    gl.uniformMatrix4fv(modelviewUniformLoc, false, modelviewMatrix);

    var fieldOfViewY = Math.PI / 4;
    aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight;
    var zNear = 1;
    var zFar = terrainSize;

    projectionMatrix[0]  = fieldOfViewY / aspectRatio;
    projectionMatrix[1]  = 0;
    projectionMatrix[2]  = 0;
    projectionMatrix[3]  = 0;

    projectionMatrix[4]  = 0;
    projectionMatrix[5]  = fieldOfViewY;
    projectionMatrix[6]  = 0;
    projectionMatrix[7]  = 0;

    projectionMatrix[8]  = 0;
    projectionMatrix[9]  = 0;
    projectionMatrix[10] = (zNear + zFar) / (zNear - zFar);
    projectionMatrix[11] = -1;

    projectionMatrix[12] = 0;
    projectionMatrix[13] = 0;
    projectionMatrix[14] = 2 * zNear * zFar / (zNear - zFar);
    projectionMatrix[15] = 0;

    gl.uniformMatrix4fv(projectionUniformLoc, false, projectionMatrix);

    gl.uniform1i(grassTextureSamplerUniformLoc, 0);

    gl.clearColor(0.4, 0.6, 1.0, 0.5);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    gl.drawElements(gl.TRIANGLES, (terrainSize - 1) * (terrainSize - 1) * 6, gl.UNSIGNED_SHORT, 0);

    if (gCurrentTimestamp == 1) {
      // First frame only - wait a bit (after rendering the scene once)
      requestAnimationFrame(function() {
        setTimeout(requestAnimationFrame, PRETEST_DELAY_MS, draw);
      });
    } else {
      requestAnimationFrame(draw);
    }
  }

  function onresize() {
    var canvas =  document.getElementById("c");
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    aspectRatio = canvas.width / canvas.height;
    gl.viewport(0, 0, canvas.width, canvas.height);
  }

  var gResults = {values: [], names: [], raw: []};

  function setupAndRun(alpha, antialias, name, doneCallback) {
    // Remove the old canvas if exists, and create a new one for the new run
    var c = document.getElementById("c");
    if (c)
        c.remove();
    c = document.createElement("canvas");
    c.id = "c";
    document.body.insertBefore(c, document.body.firstChild);

    // Trigger the run with specified args, with callback function to process the result
    startTest(alpha, antialias, function(intervals) {
        gResults.names.push(name);
        gResults.raw.push(intervals);
        setTimeout(doneCallback, 0);
    });
  }

  function reportResults(results) {
    // Format a nice human-readable report
    var msg = "";
    for (var i = 0; i < results.names.length; i++) {
      var data = results.raw[i];
      var avg   = average(data);
      gResults.values.push(avg); // This is the only "official" reported value for this set
      var sd = sampleStdDev(data);

      // Compose a nice human readable message. Not used officially anywhere.
      msg += results.names[i] + "= Average: " + avg.toFixed(2)
           + "  stddev: " + sd.toFixed(1) + " (" + (100 * sd / avg).toFixed(1) + "%)"
           + "\nIntervals: " + data.map(function(v) {
                // Display with 1 decimal point digit, and make excessively big values noticeable.
                var value = v.toFixed(1);
                // Not scientific, but intervals over 2 * average are.. undesirable.
                var threshold = avg * 2;
                return v < threshold ? value : " [_" + value + "_] ";
             }).join("  ")
           + "\n\n";
    }
    dump(msg); // Put the readable report at talos run-log

    if (window.tpRecordTime) {
      // within talos - report the results
      return tpRecordTime(results.values.join(","), 0, results.names.join(","));
    }
    // Local run in a plain browser, display the formatted report
    alert(msg);
    return undefined;
  }

  // The full test starts here
  function test() {
    // We initially hide the <body>, to reduce the chance of spinning our wheels
    // with incremental ASAP-paint-mode paints during pageload. Now that onload
    // has fired, we un-hide it:
    document.body.style.display = "";

    gResults = {values: [], names: [], raw: []};
    // This test measures average frame interval during WebGL animation as follows:
    // 1. Creates a new WebGL canvas.
    // 2. setup the scene and render 1 frame.
    // 3. Idle a bit (500ms).
    // 4. Render/Animate several (129) frames using requestAnimationFrame.
    // 5. Average the intervals of the last (100) frames <-- the result.
    //
    // The reason for the initial idle is to allow some internal cleanups (like CC/GC etc).
    // The unmeasured warmup rendering intervals are to allow Firefox to settle at a consistent rate.
    // The idle + warmup are common practices in benchmarking where we're mostly interested
    // in the stable/consistent throughput rather than in the throughput during transition state
    // from idle to iterating.

    // Run the same sequence 4 times for all combination of alpha and antialias
    // (Not using promises chaining for better compatibility, ending up with nesting instead)
    // talos unfortunately trims common prefixes, so we prefix with a running number to keep the full name
    setupAndRun(false, false, "0.WebGL-terrain-alpha-no-AA-no", function() {
      setupAndRun(false, true, "1.WebGL-terrain-alpha-no-AA-yes", function() {
        setupAndRun(true, false, "2.WebGL-terrain-alpha-yes-AA-no", function() {
          setupAndRun(true, true, "3.WebGL-terrain-alpha-yes-AA-yes", function() {
            reportResults(gResults);
          });
        });
      });
    });
  }
</script>
</head>
<body onload="test();" style="overflow:hidden; margin:0; display:none">
<canvas id="c"></canvas>
<img src="grass.jpeg" style="display:none" id="grass"/>
</body>
